/**
 * Copyright 2007 American Well Systems
 * All rights reserved.
 *
 * It is illegal to use, reproduce or distribute
 * any part of this Intellectual Property without
 * prior written authorization from American Well.
 */
package com.primera.geneiousplugin.python;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import jebl.util.ProgressListener;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

import com.biomatters.geneious.publicapi.databaseservice.DatabaseServiceException;
import com.biomatters.geneious.publicapi.databaseservice.WritableDatabaseService;
import com.biomatters.geneious.publicapi.documents.AnnotatedPluginDocument;
import com.biomatters.geneious.publicapi.documents.DocumentUtilities;
import com.biomatters.geneious.publicapi.documents.PluginDocument;
import com.biomatters.geneious.publicapi.documents.URN;
import com.biomatters.geneious.publicapi.documents.XMLSerializationException;
import com.biomatters.geneious.publicapi.documents.XMLSerializer;
import com.biomatters.geneious.publicapi.plugin.DocumentOperation;
import com.biomatters.geneious.publicapi.plugin.DocumentOperationException;
import com.biomatters.geneious.publicapi.plugin.DocumentSelectionSignature;
import com.biomatters.geneious.publicapi.plugin.GeneiousActionOptions;
import com.biomatters.geneious.publicapi.plugin.Icons;
import com.biomatters.geneious.publicapi.plugin.Options;
import com.biomatters.geneious.publicapi.plugin.PluginUtilities;
import com.biomatters.geneious.publicapi.plugin.GeneiousActionOptions.MainMenu;
import com.primera.geneiousplugin.ui.CompletionDialog;
import com.primera.geneiousplugin.ui.Console;
import com.primera.geneiousplugin.ui.ResourceManager;


/**
 * A Geneious {@link DocumentOperation} for running a Python script against a set of documents.
 * @author Rich
 */
public class RunPythonOperation extends DocumentOperation {

	private ExecutorService  executor = Executors.newCachedThreadPool();

	/**
	 * {@inheritDoc}
	 */
	@Override
	public GeneiousActionOptions getActionOptions() {
		return new GeneiousActionOptions(ResourceManager.getMsg("action.name"),
				ResourceManager.getMsg("action.description"),
				new Icons(ResourceManager.getIcon("py.small"), ResourceManager.getIcon("py.large")))
			.setInMainToolbar(true)
			.setInPopupMenu(true)
			.setMainMenuLocation(MainMenu.Tools)
			.setProOnly(false)
			.setToolbarName(ResourceManager.getMsg("toolbar.name"));
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getHelp() {
		return ResourceManager.getMsg("help.html");
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public DocumentSelectionSignature[] getSelectionSignatures() {
		return new DocumentSelectionSignature[] {
				new DocumentSelectionSignature(PluginDocument.class, 1, Integer.MAX_VALUE)};
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Options getOptions(AnnotatedPluginDocument... documents)
			throws DocumentOperationException {
		return new PythonOptions();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<AnnotatedPluginDocument> performOperation(
			final AnnotatedPluginDocument[] annotatedDocuments,
			final ProgressListener progressListener,
			final Options inO)
			throws DocumentOperationException {
		List<PluginDocument> r = null;
		WritableDatabaseService newFolder = null;
		final CompletionDialog dialog = new CompletionDialog();
		final PythonOptions options = (PythonOptions)inO;
		progressListener.setProgress(0);

		try {

			String folderName = options.getNewFolder();
			if (folderName != null && folderName.trim().length() != 0) {
				newFolder = createNewFolder(annotatedDocuments[0], folderName);
				if (newFolder == null) {
					dialog.write("Unable to create result folder.  Maybe the source documents are not in the local database?");
					dialog.write("Results will be written to the same folder");
				}
			}
			else {
				dialog.write("No folder name specified. Writing results to the same folder.");
			}


			Future<List<PluginDocument>> future =
				executor.submit(new Callable<List<PluginDocument>>() {
					public List<PluginDocument> call() throws Exception {
						return runPy(annotatedDocuments, options, progressListener, dialog);
					}
				});

			try {
				r = future.get();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}

			progressListener.setMessage("Execution finished. Moving documents...");
			progressListener.setProgress(.75);

			if (r != null && newFolder != null) {
				List<AnnotatedPluginDocument> newDocs = DocumentUtilities.createAnnotatedPluginDocuments(r);
				for (AnnotatedPluginDocument apd : newDocs) {
					newFolder.addDocumentCopy(apd, progressListener);
				}
			}
		} catch (DatabaseServiceException e) {
			dialog.write("Error creating output folder. " + e);
			printStackTrace(dialog, e);
		}

		progressListener.setProgress(1);
		// TODO get on the swing thread!!
		// this will block
		dialog.setDocumentsCreated(r != null ? r.size() : 0);
		dialog.setVisible(true);


		// if we couldn't create a new folder and had valid results, return them and they
		// will be created in the same folder
		if (newFolder == null && r != null) {
			return DocumentUtilities.createAnnotatedPluginDocuments(r);
		}
		// otherwise we created them ourselves in the new location so return null
		return null;
	}

	/**
	 * Create a new folder in geneious local database
	 */
	private WritableDatabaseService createNewFolder(AnnotatedPluginDocument base, String name)
		throws DatabaseServiceException {
		URN urn = base.getURN();
		WritableDatabaseService dbs = (WritableDatabaseService)PluginUtilities.getGeneiousService("LocalDocuments");
		WritableDatabaseService subFolder = dbs.getDocumentLocation(urn);
		if (subFolder == null) {
			return null;
		}
		return subFolder.createChildFolder(name);
	}


	// do the deed


	private List<PluginDocument> runPy(final AnnotatedPluginDocument[] docs,
			final PythonOptions options, final ProgressListener pl, final Console console)
				throws DocumentOperationException {

		List<PluginDocument> responseDocs = new ArrayList<PluginDocument>();
		try {
			OutputStream out = null;
			InputStream in = null;
			BufferedReader inErr = null;
			try {

				pl.setMessage("Running script: " + options.debug());
				sleep(2);

				// kick of the java process
				Process process = Runtime.getRuntime().exec(new String[] {
						options.getPythonCommand(),
						options.getScriptLocation(),
						options.getArguments()
					});

				// serialize the docs and pipe them into python

				XMLOutputter xmlOut = new XMLOutputter();
	            out = new BufferedOutputStream(process.getOutputStream());
	            for (AnnotatedPluginDocument apd : docs) {
	            	PluginDocument doc = apd.getDocument();
	            	Element xml = XMLSerializer.classToXML("document", doc);
	            	xmlOut.output(xml, out);
	            }
	            out.flush();
	            out.close();


	            //read back any results and build new documents
	            pl.setProgress(.25);

	            in = new BufferedInputStream(process.getInputStream());
	            inErr = new  BufferedReader(new InputStreamReader(process.getErrorStream()));

	            final BufferedReader _inErr = inErr;
	            executor.submit(new Callable<Object>() {
	            	public Object call() throws Exception {
	            		// pipe std err to the console
	            		String line = _inErr.readLine();
	            		while (line != null) {
	            			console.write(line);
	            			line = _inErr.readLine();
	            		}
	            		return null;
	            	}
	            });

	            // build response documents from proceses stdout
	            SAXBuilder sax = new SAXBuilder();
	            Document response = sax.build(in);
	            Element root = response.getRootElement();
	            for (Object o : root.getChildren()) {
	            	Element e = (Element)o;
	            	PluginDocument pd =( PluginDocument)XMLSerializer.classFromXML(e);
	            	responseDocs.add(pd);
	            }
	            in.close();

	            pl.setMessage("Done processing python output.");
	            pl.setProgress(.9);

	            int status = process.exitValue();
	            console.write("Python exited with status: " + status);
	            if (status != 0) {
	            	// if python barffed we can't trust that the results are valid
	            	return null;
	            }
			}
			finally {
				if (out != null) {
					out.close();
				}
				if (in != null) {
					in.close();
				}
				if (inErr != null) {
					inErr.close();
				}
			}
        }
		catch (final IOException e) {
			console.write("Problem executing Python script: ." + options.debug());
			printStackTrace(console, e);
			return null;
        }
        catch (final XMLSerializationException e) {
        	console.write("Problem unmarhshaling XML response.");
        	printStackTrace(console, e);
        	return null;
        }
        catch (final JDOMException e) {
        	console.write("Problem unmarhshaling XML response.");
        	printStackTrace(console, e);
        	return null;
		}
        return responseDocs;
	}

	private void printStackTrace(Console console, Exception e) {
		for (StackTraceElement ste : e.getStackTrace()) {
			console.write(ste.toString());
		}
	}

	private static void sleep(int s) {
//		try {
//			Thread.sleep(s * 1000);
//		} catch (InterruptedException e) {
//			e.printStackTrace();
//		}
	}

}
